import * as changeCase from 'change-case';
import { castArray, omit, isArray, isEmpty, isFunction, uniq, isObject, dedent } from 'vtils';
import fs from 'fs-extra';
import path from 'path';
import ora, { Ora } from 'ora';
import JSON5 from 'json5';
import dayjs from 'dayjs';

import { getProjectExport } from './apis';
import * as Types from '../../types';
import {
  sleep,
  promiseIterator,
  codeFormat,
  apiPathParse,
  jsonSchemaToType,
  propDefinitionsToJsonSchema,
  jsonSchemaStringToJsonSchema,
  jsonToJsonSchema,
  mockjsTemplateToJsonSchema,
  processJsonSchema,
  transformSchemaNumber
} from '../../utils';
import logger, { prefix } from '../../utils/logger';
import { interfaceCodeTemplate, fetchApiCodeTemplate, fetchApiModulesImport } from '../template';
import type { JSONSchema4 } from 'json-schema';

// 枚举
const { Method, Required, RequestBodyType, RequestFormItemType, ResponseBodyType } = Types;
// 当前当前项目目录
const appDir = fs.realpathSync(process.cwd());
// 输出路径
const resolveFilePath = (_path: string) => path.resolve(appDir, _path);
// 截取部分信息
const getPartOfInterfaceInfo = (interfaceInfo: Types.Interface) => {
  const keys: (keyof Types.Interface)[] = 'method _id catid project_id path title status'.split(
    ' '
  );
  return (() => {
    const data: Partial<Types.Interface> = {};
    keys.forEach((key) => {
      data[key] = interfaceInfo[key];
    });
    return data;
  })();
};
// 匹配花括号正则
const BEARD_REG = /\{(.+?)\}/g;

export default class Generator {
  config: Types.IServerConfig[];

  private spinner: Ora;

  private outputDirs: Types.OutputFilePath;

  constructor(config: Types.IServerConfig | Types.IServerConfig[]) {
    this.outputDirs = {};
    this.spinner = ora({
      prefixText: `${[prefix]}`
    });

    this.config = castArray(config).map((item) => {
      if (item.serverUrl) {
        // 去除地址后面的 '/'
        item.serverUrl = item.serverUrl.replace(/\/+$/, '');
      }
      item.outputFilePath = resolveFilePath(item.outputFilePath || './src/api');
      return item;
    });
  }

  private spinnerStart(text?: string) {
    if (text) {
      this.spinner.text = text;
    }
    this.spinner.start();
  }

  /**
   * 生成
   */
  async generate() {
    this.outputDirs = {};

    this.spinnerStart('正在拉取项目...');

    // 存放待处理的解析任务
    const catHandler: (() => Promise<void>)[] = [];
    const compileCatFunc = (category: Types.Category, projectInfo: Types.TMixConfig) => {
      const handleArray = (data?: number | number[]) => {
        return uniq(castArray(data).filter(Boolean));
      };
      const _excludeCatIds = handleArray(projectInfo.excludeCatIds);
      const _excludeApiIds = handleArray(projectInfo.excludeApiIds);
      // 优先处理 include 数据
      const _includeCatIds = handleArray(projectInfo.includeCatIds);
      const _includeApiIds = handleArray(projectInfo.includeApiIds);
      // 是否存在 include
      const hasInclude = _includeCatIds.length + _includeApiIds.length > 0;
      // 是否存在 exclude
      const hasExclude = _excludeCatIds.length + _excludeApiIds.length > 0;

      const handlePutApiCode = async (interfaceInfo: Types.Interface) => {
        // 选择生成或者排除生成
        const apiData = await this.generateInterfaceCode(projectInfo, interfaceInfo);
        // 支持自定义输出接口目录，完整的绝对路径
        const _outputFileDir = isFunction(projectInfo.getApiDir)
          ? projectInfo.getApiDir(getPartOfInterfaceInfo(interfaceInfo), changeCase as any)
          : '';
        // 获取文件路径
        const getFilePath = (file: string) => {
          return path.join(
            projectInfo.outputFilePath!,
            _outputFileDir || apiData.parsedPath.dir,
            file
          );
        };
        // 输出数据类型文件路径
        const outputFilePath = getFilePath('types.ts');

        if (!this.outputDirs[outputFilePath]) {
          this.outputDirs[outputFilePath] = {
            config: {
              ...projectInfo
            },
            content: []
            // apiFilePath: projectInfo.apiFilePath
            //   ? resolveFilePath(projectInfo.apiFilePath)
            //   : resolveFilePath('./api/request.ts')
          };
        }
        this.outputDirs[outputFilePath].content.push(apiData.code);
        // 只生产types就不管
        if (!projectInfo.typesOnly) {
          // 输出api接口文件路径
          const outputApiPath = getFilePath('index.ts');

          if (!this.outputDirs[outputApiPath]) {
            this.outputDirs[outputApiPath] = {
              config: {
                ...projectInfo
              },
              content: [
                dedent`
                  ${fetchApiModulesImport}
                  import * as Types from './types';
                `
              ]
            };
          }
          this.outputDirs[outputApiPath].content.push(apiData.apiCode);
        }
      };

      catHandler.push(
        ...category.list.map((interfaceInfo) => {
          return async () => {
            // 有include数据只处理include的
            if (hasInclude) {
              if (
                _includeCatIds.includes(interfaceInfo.catid) ||
                _includeApiIds.includes(interfaceInfo._id)
              ) {
                await handlePutApiCode(interfaceInfo);
              }
            }
            // 进行过滤，排除不需要的接口、分类
            else if (hasExclude) {
              if (
                _excludeApiIds.includes(interfaceInfo._id) ||
                _excludeCatIds.includes(interfaceInfo.catid)
              ) {
                return;
              }
              await handlePutApiCode(interfaceInfo);
            } else {
              await handlePutApiCode(interfaceInfo);
            }
          };
        })
      );
    };

    await promiseIterator(this.config, async (item, i) => {
      const { serverUrl } = item;
      // 没有配url也不管
      if (!serverUrl) {
        logger.warn(`第${i}项: serverUrl 有误, 该服务所属全部项目将被忽略`);
        return;
      }

      await promiseIterator(item.projects, async (project, i2) => {
        // 项目配置信息
        const projectInfo = Object.assign(
          {
            ...project
          },
          omit(item, ['projects']),
          {
            // 输出路径
            outputFilePath: resolveFilePath(item.outputFilePath!)
          }
        );
        // 没有token就不管
        if (!project.token) {
          logger.warn(`第${i}项 项目${i2}: token 有误`);
          return;
        }
        // 请求项目
        const categoryList = await getProjectExport(serverUrl, project.token);

        categoryList.forEach((category) => {
          compileCatFunc(category, projectInfo);
        });
      });
    });

    await promiseIterator(catHandler);
    this.spinner.text = '正在写入文件...';
    await sleep();
    await this.write();
    this.spinner.stop();

    if (Object.keys(this.outputDirs).length) {
      logger.success('文件已生成');
    } else {
      logger.warn('无生成内容');
    }
  }

  /* 获取各种名称 */
  static generateName(pathName: string) {
    // 方法名称
    const requestFunctionName = changeCase.camelCase(`${pathName}Api`);
    const requestDataTypeName = changeCase.pascalCase(`${requestFunctionName}Req`);
    const responseDataTypeName = changeCase.pascalCase(`${requestFunctionName}Res`);

    return {
      requestFunctionName,
      requestDataTypeName,
      responseDataTypeName
    };
  }

  /**
   * 生成请求数据类型
   * @param interfaceInfo
   * @param typeName
   */
  async generateRequestDataType(interfaceInfo: Types.Interface, typeName: string): Promise<string> {
    let jsonSchema!: JSONSchema4;

    switch (interfaceInfo.method) {
      case Method.GET:
      case Method.HEAD:
      case Method.OPTIONS:
        jsonSchema = propDefinitionsToJsonSchema(
          interfaceInfo.req_query.map<Types.PropDefinition>((item) => {
            return {
              name: item.name,
              required: item.required === Required.true,
              type: 'string',
              comment: item.desc
            };
          })
        );
        break;
      default:
        switch (interfaceInfo.req_body_type) {
          case RequestBodyType.form:
            jsonSchema = propDefinitionsToJsonSchema(
              interfaceInfo.req_body_form.map<Types.PropDefinition>((item) => {
                return {
                  name: item.name,
                  required: item.required === Required.true,
                  type: (item.type === RequestFormItemType.file ? 'file' : 'string') as any,
                  comment: item.desc
                };
              })
            );
            break;
          case RequestBodyType.json:
            if (interfaceInfo.req_body_other) {
              try {
                const reqBodyOtherData = JSON.parse(interfaceInfo.req_body_other);
                if (reqBodyOtherData.properties) {
                  reqBodyOtherData.properties = transformSchemaNumber(
                    reqBodyOtherData.properties as Record<string, JSONSchema4>
                  );
                  interfaceInfo.req_body_other = JSON.stringify(reqBodyOtherData);
                }
              } catch (_) {
                /* nothing todo */
              }

              // 获取 json schema
              jsonSchema = interfaceInfo.req_body_is_json_schema
                ? // 已经是 json schema 字符串
                  jsonSchemaStringToJsonSchema(interfaceInfo.req_body_other)
                : // 不是 json schema，通过json转换一下
                  jsonToJsonSchema(JSON5.parse(interfaceInfo.req_body_other));
            }
            break;
          default:
            break;
        }
        break;
    }

    if (isArray(interfaceInfo.req_params) && interfaceInfo.req_params.length) {
      const paramsJsonSchema = propDefinitionsToJsonSchema(
        interfaceInfo.req_params.map<Types.PropDefinition>((item) => {
          return {
            name: item.name,
            required: true,
            type: 'string',
            comment: item.desc
          };
        })
      );

      if (jsonSchema) {
        jsonSchema.properties = {
          ...jsonSchema.properties,
          ...paramsJsonSchema.properties
        };
        jsonSchema.required = [
          ...(jsonSchema.required || []),
          ...(paramsJsonSchema.required || [])
        ];
      } else {
        jsonSchema = paramsJsonSchema;
      }
    }
    return await this.abstractDataType(jsonSchema, typeName);
  }

  /**
   * types 拆分
   */
  async abstractDataType(jsonSchema: JSONSchema4, typeName: string) {
    // 结果
    const resTypesDatas = [];
    // 处理 properties
    async function handleProperties(
      properties: {
        [k: string]: JSONSchema4;
      },
      typeKeyName: string
    ) {
      // const _keys = Object.keys(properties);
      // await promiseIterator(_keys, async (_key) => {
      //   const jsonSchema4 = properties[_key];
      //   if (jsonSchema4.type === 'array') {
      //     return await handleItems(jsonSchema4, `${_key}res`);
      //   } else if (jsonSchema4.type === 'object' && jsonSchema4.properties) {
      //     const _name = `${_key}res`;
      //     jsonSchema4.tsType = _name;
      //     return await handleProperties(jsonSchema4.properties, _name);
      //   }
      //   resTypesData += (await jsonSchemaToType(jsonSchema4, typeKeyName)) + '\n';
      // });
      resTypesDatas.push(jsonSchemaToType(properties, typeKeyName));
    }

    // 处理items，数组才有
    async function handleItems(items: JSONSchema4 | JSONSchema4[], typeKeyName: string) {
      if (isArray(items)) {
        await promiseIterator(items, async (_item, i) => {
          await handleItems(_item, `${typeKeyName}${i}Res`);
        });
      } else if (items.properties) {
        await handleProperties(items, typeKeyName);
      }
    }

    if (jsonSchema?.type === 'object') {
      await promiseIterator(Object.keys(jsonSchema.properties || {}), async (typeKey) => {
        const _schema4Data = jsonSchema.properties![typeKey];

        if (_schema4Data) {
          const _typeKey = changeCase.camelCase(`${typeName}/${typeKey}Res`);

          if (_schema4Data?.type === 'array') {
            if (_schema4Data.items) {
              if (isArray(_schema4Data.items) || _schema4Data.items?.type === 'object') {
                const _keyName = `${_typeKey}Item`;
                _schema4Data.tsType = _keyName + '[]';

                await handleItems(_schema4Data.items, _keyName);
              }
            }
          } else if (_schema4Data?.type === 'object') {
            if (_schema4Data.properties) {
              const cloneSchema4Data = { ..._schema4Data };
              _schema4Data.tsType = _typeKey;
              return await handleProperties(cloneSchema4Data, _typeKey);
            }
          }
        }
      });
    } else if (jsonSchema?.type === 'array' && jsonSchema.items) {
      jsonSchema.tsType = `${typeName}Item[]`;
      await handleItems(jsonSchema.items, `${typeName}Item`);
    }

    resTypesDatas.push(jsonSchemaToType(jsonSchema, typeName));

    const codes = await Promise.all(resTypesDatas);
    return codes.join('\n');
  }

  /**
   * 生成响应数据类型
   */
  async generateResponseDataType(
    interfaceInfo: Types.Interface,
    typeName: string,
    dataKey?: string
  ): Promise<string> {
    let jsonSchema: JSONSchema4 = {};

    switch (interfaceInfo.res_body_type) {
      case ResponseBodyType.json:
        if (interfaceInfo.res_body) {
          // json反序列化的res数据
          const _resBodyData = transformSchemaNumber(JSON.parse(interfaceInfo.res_body));
          jsonSchema = interfaceInfo.res_body_is_json_schema
            ? processJsonSchema(_resBodyData)
            : mockjsTemplateToJsonSchema(_resBodyData);
        }
        break;
      default:
        return `export type ${typeName} = any`;
    }

    if (dataKey && jsonSchema && jsonSchema.properties && jsonSchema.properties[dataKey]) {
      jsonSchema = jsonSchema.properties[dataKey];
    }
    return await this.abstractDataType(jsonSchema, typeName);
  }

  /**
   * 生成请求api
   */
  async generateFetchFunction(
    fetchData: Types.GetApiNamesType,
    interfaceInfo: Types.Interface,
    config: Types.TMixConfig
  ) {
    // 经过处理的path
    const path =
      ((): string | void => {
        let _path = interfaceInfo.path;

        if (fetchData.paramNames.length) {
          const matchBeard = _path.match(BEARD_REG);

          if (matchBeard?.length) {
            matchBeard.forEach((matchRes) => {
              _path = _path.replace(matchRes, `$${matchRes}`);
            });
            return `\`${_path}\``;
          }
        }
      })() || "'" + interfaceInfo.path + "'";

    // 获取模板函数
    const apiTempHandler = isFunction(config.getApiTemplate)
      ? config.getApiTemplate
      : fetchApiCodeTemplate;

    return apiTempHandler(fetchData, {
      ...interfaceInfo,
      path
    });
  }

  /**
   * 生成代码
   * 每个接口都会调用一次
   */
  async generateInterfaceCode(config: Types.TMixConfig, interfaceInfo: Types.Interface) {
    const extendedInterfaceInfo = {
      ...interfaceInfo,
      parsedPath: apiPathParse(interfaceInfo.path)
    };
    // 使用配置获取api名称
    const apiNamesData = isFunction(config.getApiNames)
      ? config.getApiNames(getPartOfInterfaceInfo(interfaceInfo), changeCase as any) ?? {}
      : {};
    // 获取请求、响应数据类型名称
    const { requestDataTypeName, responseDataTypeName, requestFunctionName } = Object.assign(
      Generator.generateName(interfaceInfo.method + '/' + extendedInterfaceInfo.parsedPath.path),
      isObject(apiNamesData) ? apiNamesData : {}
    );

    // 生成入参数据类型
    const requestDataType = await this.generateRequestDataType(interfaceInfo, requestDataTypeName);
    const responseDataType = await this.generateResponseDataType(
      interfaceInfo,
      responseDataTypeName,
      config.dataKey || 'data'
    );

    const isRequestDataOptional = /(\{\}|any)$/s.test(requestDataType);

    // 支持路径参数
    const paramNames = (interfaceInfo.req_params || []).map((item) => {
      return item.name;
    });
    // const paramNamesLiteral = JSON.stringify(paramNames);
    // const paramNameType = paramNames.length === 0 ? 'string' : `'${paramNames.join("' | '")}'`;

    // 转义标题中的 /
    const escapedTitle = ('' + interfaceInfo.title).replace(/\//g, '\\/');

    // 接口标题 + yapi地址
    const interfaceApiTitle = `${escapedTitle}`;
    const interfaceApiUrl = `${config.serverUrl}/project/${interfaceInfo.project_id}/interface/api/${interfaceInfo._id}`;

    // 接口摘要
    const interfaceSummary: {
      label: string;
      value: string | string[];
    }[] = [
      {
        label: '请求方法:',
        value: interfaceInfo.method.toUpperCase()
      },
      {
        label: '请求地址:',
        value: interfaceInfo.path
      },
      {
        label: '更新时间:',
        value: `${dayjs(interfaceInfo.up_time * 1000).format('YYYY-MM-DD HH:mm:ss')}`
      }
    ];
    const interfaceExtraComments = interfaceSummary
      .filter((item) => {
        return !isEmpty(item.value);
      })
      .map((item) => {
        return `* @${item.label} ${castArray(item.value).join(', ')}`;
      })
      .join('\n');

    const code = interfaceCodeTemplate({
      interfaceApiTitle,
      interfaceApiUrl,
      interfaceExtraComments,
      requestDataType,
      responseDataType,
      // paramNameType,
      // paramNamesLiteral,
      requestDataTypeName,
      responseDataTypeName,
      // interfaceInfo,
      isRequestDataOptional
    });

    // 此处为生成api代码
    const apiCode = config.typesOnly
      ? ''
      : await this.generateFetchFunction(
          {
            requestDataTypeName,
            responseDataTypeName,
            requestFunctionName,
            interfaceApiTitle,
            interfaceApiUrl,
            interfaceExtraComments,
            requestDataType,
            isRequestDataOptional,
            paramNames
          },
          interfaceInfo,
          config
        );

    return {
      apiCode,
      code,
      parsedPath: extendedInterfaceInfo.parsedPath
    };
  }

  // 写文件
  async write() {
    // 路径
    const filePaths = Object.keys(this.outputDirs);
    await promiseIterator(filePaths, async (outputFilePath) => {
      const { content } = this.outputDirs[outputFilePath];
      const rawOutputContent = content.join('\n\n').trim();
      const outputContent = codeFormat(rawOutputContent, {
        parser: 'typescript',
        printWidth: 100,
        tabWidth: 2,
        singleQuote: true,
        semi: true,
        trailingComma: 'all',
        bracketSpacing: false,
        endOfLine: 'auto'
      });

      await fs.outputFile(outputFilePath, outputContent);
    });
  }
}
